/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.bunjlabs.fuga.network.netty; import com.bunjlabs.fuga.FugaApp; import com.bunjlabs.fuga.foundation.content.BufferedContent; import com.bunjlabs.fuga.foundation.Cookie; import com.bunjlabs.fuga.foundation.Request; import com.bunjlabs.fuga.foundation.http.RequestMethod; import com.bunjlabs.fuga.foundation.Response; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpChunkedInput; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import io.netty.handler.stream.ChunkedStream; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; class NettyHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> { private final Logger log = LogManager.getLogger(NettyHttpServerHandler.class); private final FugaApp app; private final String serverVersion; private final int forwarded; private ByteBuf contentBuffer; private HttpRequest httprequest; private Request.Builder requestBuilder; private boolean decoder; NettyHttpServerHandler(FugaApp app) { this.app = app; this.contentBuffer = Unpooled.buffer(); this.serverVersion = app.getConfiguration().get("fuga.version"); this.forwarded = app.getConfiguration().get("fuga.http.forwarded").equals("rfc7239") ? 1 : 0; } private void reset() { this.httprequest = null; this.requestBuilder = null; this.decoder = false; this.contentBuffer = Unpooled.buffer(); } @Override public void channelRead0(ChannelHandlerContext ctx, HttpObject msg) { if (msg instanceof HttpRequest) { this.httprequest = (HttpRequest) msg; requestBuilder = new Request.Builder(); requestBuilder.requestMethod(RequestMethod.valueOf(httprequest.method().name())) .uri(httprequest.uri()); try { // Decode URI GET query parameters QueryStringDecoder queryStringDecoder = new QueryStringDecoder(httprequest.uri()); requestBuilder.path(queryStringDecoder.path()).query(queryStringDecoder.parameters()); // Process cookies List<Cookie> cookies = new ArrayList<>(); String cookieString = httprequest.headers().get(HttpHeaderNames.COOKIE); if (cookieString != null) { ServerCookieDecoder.STRICT.decode(cookieString).stream().forEach((cookie) -> { cookies.add(NettyCookieConverter.convertToFuga(cookie)); }); } requestBuilder.cookies(cookies); // Process headers Map<String, String> headers = new HashMap<>(); httprequest.headers().entries().stream().forEach((e) -> { headers.put(e.getKey(), e.getValue()); }); requestBuilder.headers(headers); // Get real parameters from frontend HTTP server boolean isSecure = false; SocketAddress remoteAddress = ctx.channel().remoteAddress(); if (forwarded == 1) { // RFC7239 if (headers.containsKey("Forwarded")) { String fwdStr = headers.get("Forwarded"); List<String> fwdparams = Stream.of(fwdStr.split("; ")).map((s) -> s.trim()).collect(Collectors.toList()); for (String f : fwdparams) { String p[] = f.split("="); switch (p[0]) { case "for": remoteAddress = parseAddress(p[1]); break; case "proto": isSecure = p[1].equals("https"); break; } } } } else if (forwarded == 0) { // X-Forwarded if (headers.containsKey("X-Forwarded-Proto")) { if (headers.get("X-Forwarded-Proto").equalsIgnoreCase("https")) { isSecure = true; } } if (headers.containsKey("X-Forwarded-For")) { String fwdfor = headers.get("X-Forwarded-For"); remoteAddress = parseAddress(fwdfor.contains(",") ? fwdfor.substring(0, fwdfor.indexOf(',')) : fwdfor); } else if (headers.containsKey("X-Real-IP")) { remoteAddress = parseAddress(headers.get("X-Real-IP")); } } requestBuilder.remoteAddress(remoteAddress).isSecure(isSecure); if (headers.containsKey("Accept-Language")) { String acceptLanguage = headers.get("Accept-Language"); List<Locale> acceptLocales = Stream.of(acceptLanguage.split(",")) .map((s) -> s.contains(";") ? s.substring(0, s.indexOf(";")).trim() : s.trim()) .map((s) -> s.contains("-") ? new Locale(s.split("-")[0], s.split("-")[0]) : new Locale(s)) .collect(Collectors.toList()); requestBuilder.acceptLocales(acceptLocales); } // if (httprequest.method().equals(HttpMethod.GET)) { processResponse(ctx); return; } decoder = true; } catch (Exception e) { processClientError(ctx, requestBuilder.build(), 400); return; } } if (msg instanceof HttpContent && decoder) { HttpContent httpContent = (HttpContent) msg; contentBuffer.writeBytes(httpContent.content()); if (httpContent instanceof LastHttpContent) { requestBuilder.content(new BufferedContent(contentBuffer.nioBuffer())); processResponse(ctx); } } } private void processClientError(ChannelHandlerContext ctx, Request request, int code) { Response response = app.getErrorHandler().onClientError(request, code); writeResponse(ctx, request, response); reset(); } private void processServerError(ChannelHandlerContext ctx, Request request, Throwable cause) { Response response = app.getErrorHandler().onServerError(request, cause); writeResponse(ctx, request, response); reset(); } private void processResponse(ChannelHandlerContext ctx) { Request request = requestBuilder.build(); Response response = app.getRequestHandler().onRequest(request); if (response == null) { log.error("Null response is received"); processServerError(ctx, request, new NullPointerException("Null response is received")); return; } writeResponse(ctx, request, response); reset(); } private void writeResponse(ChannelHandlerContext ctx, Request request, Response response) { HttpResponse httpresponse = new DefaultHttpResponse( HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status())); httpresponse.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); httpresponse.headers().set(HttpHeaderNames.CONTENT_TYPE, response.contentType()); // Disable cache by default httpresponse.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache, no-store, must-revalidate, max-age=0"); httpresponse.headers().set(HttpHeaderNames.PRAGMA, "no-cache"); httpresponse.headers().set(HttpHeaderNames.EXPIRES, "0"); response.headers().entrySet().stream().forEach((e) -> httpresponse.headers().set(e.getKey(), e.getValue()) ); httpresponse.headers().set(HttpHeaderNames.SERVER, "Fuga Netty Web Server/" + serverVersion); // Set cookies httpresponse.headers().set( HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode( NettyCookieConverter.convertListToNetty(response.cookies()) ) ); if (response.length() >= 0) { httpresponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.length()); } if (HttpUtil.isKeepAlive(httprequest)) { httpresponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); } else { httpresponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); } ctx.write(httpresponse); if (response.stream() != null) { ctx.write(new HttpChunkedInput(new ChunkedStream(response.stream()))); } LastHttpContent fs = new DefaultLastHttpContent(); ChannelFuture sendContentFuture = ctx.writeAndFlush(fs); if (!HttpUtil.isKeepAlive(httprequest)) { sendContentFuture.addListener(ChannelFutureListener.CLOSE); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { log.catching(cause); ctx.close(); reset(); } private static InetSocketAddress parseAddress(String addr) { String ip = addr.contains(":") ? addr.substring(0, addr.lastIndexOf(':')) : addr; String port = addr.contains(":") ? addr.substring(addr.lastIndexOf(':') + 1) : "0"; return new InetSocketAddress(ip, Integer.parseInt(port)); } }